// (c) Copyright 2013 Mike Riedel.
// This source is subject to the Microsoft Public License (Ms-PL).
// All other rights reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

namespace FreshMeat.ErrorHandlingBlock
{
   /// <summary>
   /// Handles errors that may occur when an action is performed.
   /// </summary>
   public class ErrorHandler
   {
      readonly List<IErrorPolicy> policies;

      /// <summary>
      /// Creates instance with a variable number of policies.
      /// </summary>
      /// <param name="policies"></param>
      public ErrorHandler(params ErrorPolicy[] policies)
         : this((IEnumerable<IErrorPolicy>)policies)
      {
      }

      /// <summary>
      /// Creates instance based on a collection of policies.
      /// </summary>
      /// <param name="policies"></param>
      public ErrorHandler(IEnumerable<IErrorPolicy> policies)
      {
         this.policies = new List<IErrorPolicy>(policies);
      }

      /// <summary>
      /// Adds a variable number of policies to the existing policies.
      /// </summary>
      public void Add(params ErrorPolicy[] collection)
      {
         policies.AddRange(collection);
      }

      /// <summary>
      /// Adds a collection of policies to the existing policies.
      /// </summary>
      public void Add(IEnumerable<IErrorPolicy> collection)
      {
         policies.AddRange(collection);
      }

      /// <summary>
      /// Runs an action with error handling as defined by the policies.
      /// </summary>
      /// <param name="action">The action to run.</param>
      /// <param name="state">Optional state passed to the <see cref="Feedback"/> event.</param>
      [DebuggerNonUserCode]
      [DebuggerHidden]
      public async Task Run(Func<Task> action, object state = null)
      {
         var states = new Dictionary<IErrorPolicy, PolicyState>();

         while (true)
         {
            ErrorPolicyResult result;
            try
            {
               await action();
               return;
            }
            catch (Exception error)
            {
               result = Process(states, error, state);
               if (!result.ShouldRetry)
               {
                  if (result.Diagnosis == error)
                     throw;
                  throw result.Diagnosis;
               }
            }

            await TaskEx.Delay(result.TimeUntilRetry);
         }
      }

      /// <summary>
      /// Runs an action with error handling as defined by the policies.
      /// </summary>
      /// <param name="action">The action to run.</param>
      /// <param name="state">Optional state passed to the <see cref="Feedback"/> event.</param>
      [DebuggerNonUserCode]
      [DebuggerHidden]
      public async Task<T> Run<T>(Func<Task<T>> action, object state = null)
      {
         var states = new Dictionary<IErrorPolicy, PolicyState>();

         while (true)
         {
            ErrorPolicyResult result;
            try
            {
               return await action();
            }
            catch (Exception error)
            {
               result = Process(states, error, state);
               if (!result.ShouldRetry)
               {
                  if (result.Diagnosis == error)
                     throw;
                  throw result.Diagnosis;
               }
            }

            await TaskEx.Delay(result.TimeUntilRetry);
         }
      }

      ErrorPolicyResult Process(IDictionary<IErrorPolicy, PolicyState> states, Exception error, object state)
      {
         var policy = policies.FirstOrDefault(p => p.IsApplicable(error));
         if (policy == null)
            return new ErrorPolicyResult(error); // No policy, throw error

         var attempts = IncrementAttemptsForPolicy(states, policy);

         var result = policy.Apply(error, attempts);

         if (result.ShouldRetry)
            FireFeeback(new FeedbackEventArgs(state, result.Diagnosis, attempts + 1));

         return result;
      }

      static int IncrementAttemptsForPolicy(IDictionary<IErrorPolicy, PolicyState> states, IErrorPolicy policy)
      {
         // Lookup the state associated with the policy, which allows us to maintain
         // separate attempt counts per policy.  Is this really worth it or could
         // we have a single shared count?
         PolicyState state;
         if (!states.TryGetValue(policy, out state))
         {
            state = new PolicyState();
            states.Add(policy, state);
         }

         state.Attempts++;
         return state.Attempts;
      }

      /// <summary>
      /// Fires when an attempt has failed and will be retried.
      /// </summary>
      public event EventHandler<FeedbackEventArgs> Feedback;

      void FireFeeback(FeedbackEventArgs args)
      {
         var handler = Feedback;
         if (handler != null) 
            handler(this, args);
      }

      class PolicyState
      {
         public int Attempts;
      }
   }
}